package org.jprobe.codegen;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jprobe.classpath.ClassPathInterface;
import org.jprobe.classpath.ClassPathReader;
import org.jprobe.instantiate.Instantiator;
import org.jprobe.instantiate.InstantiatorException;
import org.jprobe.utils.IOUtils;

/**
 * java code generator
 * generates an instance of any object after making it cooked and ready for use
 * an example of the code generated is as follows:
 *
 * <pre>
 * @Override public final class booleanJProbe{
 * 	@Override public boolean $() { return true; }
 * }

 * @Override public final class booleanAJProbe{
 * 	@Override public boolean[] $() { return new boolean[]{true}; }
 * }
 *
 * @Override public final class intAAJProbe{
 * 	@Override public int[][] $() { return new int[][]{{1}}; }
 * }
 *
 * @Override public final class ObjectAAAJProbe{
 * 	@Override public Object[][][] $(){ return new Object[][][]{{{new Object()}}}; }
 * }
 * </pre>
 * @author Hussain Al-Mutawa
 * @version 2.0
 * @since 1.0
 */
public class CodeGenerator implements CodeGeneratorInterface{

	/** the classpath  **/
	private transient static final ClassPathInterface CLASSPATH = ClassPathReader.instance.getClassPath();

	/** the limit for creating an object which requires the same of its own as a parameter **/
	public static transient final int RECURSION_LIMIT = 100;

	/** a map which contains the primitive types dummy values */
	private static transient final Map<Class<?>,String> PRIMITIVES = new HashMap<Class<?>,String>();

	private static transient final Map<Class<?>,Integer> RECURSION_COUNTERS = new HashMap<Class<?>,Integer>();

	static{
		PRIMITIVES.put(int.class	,"1");
		PRIMITIVES.put(char.class	,"'\u0000'");
		PRIMITIVES.put(float.class	,"1.0f");
		PRIMITIVES.put(long.class	,"1L");
		PRIMITIVES.put(double.class	,"1.0d");
		PRIMITIVES.put(byte.class	,"(byte)1");
		PRIMITIVES.put(short.class	,"(short)1");
		PRIMITIVES.put(boolean.class,"true");
		PRIMITIVES.put(String.class	,"\"Object.class\"");
		PRIMITIVES.put(Object.class	,"new Object()");
	}

	@Override public String getInstantiationCode(final Class<?>clazz){
		if(clazz==null || clazz==Void.TYPE){
			throw new CodeGenException("arguments [null,void] are not accepted");
		}

		int recursionCounter=0;

		RECURSION_COUNTERS.put(clazz, recursionCounter);

		final String classJprobeName = clazz.getSimpleName().replaceAll("\\[\\]", "A") + "JProbe";
		final String pkgName = getPackageNameForImplementation(clazz);
		String result=null;
		try{
			final String fileName = "./jprobe/src/"+(pkgName+'.'+classJprobeName).replaceAll("\\.", File.separator)+".java";
			System.out.println("{{{"+fileName+"}}}");
			result = IOUtils.readFromFileSys(fileName);
		}catch(Throwable throwable){}
		
		if(result==null){
			final ImplementationInterface impl = getImplementation(clazz);
			final String objectName = getBinaryName(clazz);
	
			result =
				"package "+impl.getPackageName()+";\n"+
				"public final class " + classJprobeName + "{\n"+
				"\tpublic " + objectName + " $()throws Throwable{"+
				"\n\t\treturn "+ impl.getCode() +";\n\t}\n"+
				'}';
		}else{
			System.out.println("+++++++++++++++++++++++++++++++++++++++++ BINGO");
		}
		RECURSION_COUNTERS.remove(clazz);

		return result;
	}

	@Override public String getSignature(final Method method) {
		final StringBuffer sbuffer = new StringBuffer();

		int counter=0;
		for(final Class<?>clazz:method.getParameterTypes()){
			sbuffer.append(getBinaryName(clazz)+" p"+(++counter)+clazz.getSimpleName().replaceAll("\\[\\]", "Array")+",");
		}
		if(sbuffer.toString().endsWith(",")){
			sbuffer.setLength(sbuffer.toString().length()-1);
		}
		return sbuffer.toString();
	}
	@Override public String getParametersDeclaration(final Class<?>[]types){
		//System.err.println(Arrays.toString(types));
		final StringBuffer params = new StringBuffer();
		//get the parameters of the initializer
		for(final Class<?>ctype:types){
			params.append(getImplementation(ctype)).append(',');
		}
		if(params.toString().endsWith(",")){
			params.setLength(params.toString().length()-1);
		}
		return params.toString();
	}
	@Override public String getArrayAsString(final Class<?> array){
		if(array==null || !array.isArray()){
			throw new CodeGenException(array+" is not array");
		}
		String result="ITEM";
		Class<?>clazz=array;
		while(clazz.isArray()){
			clazz=clazz.getComponentType();
			result='{'+result+'}';
		}
		final String item = getImplementation(clazz).getCode();
		result=result.replace("ITEM", item);
		return "new "+getBinaryName(array) + result;
	}
	@Override public String getBinaryName(final Class<?>type){
		if(type==null){
			throw new CodeGenException("arguments [null,void] are not accepted");
		}
		final String result;
		if(type==void.class || type==Void.TYPE){
			result = "void";
		}else if(type.isArray()){
			result = getBinaryName(type.getComponentType())+"[]";
		}else if(type.isPrimitive() || type.getPackage()==null){
			result = type.getSimpleName();
		}else{
			result = type.getName().replace('$', '.');
		}
		return result;
	}

	@Override public String getDummyMethodsImplementation(final Class<?> initializeWhat,Class<?>initializedBy) {
		final StringBuffer contents = new StringBuffer(100);
		final Method[]methods = new UnimplementedMethodsExplorer(initializedBy).getAbstractMethods();
		for(final Method method:methods){
			final String params = getSignature(method);
			final String var;
			final Class<?>rType = method.getReturnType();
			final String bname = getBinaryName(method.getReturnType());
			if(Void.TYPE==rType || rType==void.class){
				var = "";
			}else if(PRIMITIVES.containsKey(rType)){
				var = PRIMITIVES.get(rType);
			}else{
				var = "("+bname+")null";
			}
			contents.append(
					"\t\t\tpublic " + bname + " " + method.getName() + "("+params+"){\n"+
					"\t\t\t\treturn "+ var +";\n\t\t\t}\n"
			);
		}
		return contents.toString();
	}

	private String getPackageNameForImplementation(final Class<?>clazz){
		Class<?>comp=clazz;
		while(comp.isArray()){comp=comp.getComponentType();}
		final String result;
		if(PRIMITIVES.containsKey(comp) || comp.getPackage()==null){
			result = "impl";
		}else if(CLASSPATH.isRuntimeClass(comp)){
			result = "impl."+comp.getPackage().getName();
		}else{
			result = comp.getPackage().getName();
		}
		return result;
	}


	@Override public ImplementationInterface getImplementation(final Class<?> clazz) {
		final String classBinaryName = getBinaryName(clazz);
		int recursionCounter;
		if(RECURSION_COUNTERS.containsKey(clazz)){
			recursionCounter = RECURSION_COUNTERS.get(clazz);
		}else{
			RECURSION_COUNTERS.put(clazz, 0);
			recursionCounter = 0;
		}
		if(++recursionCounter>RECURSION_LIMIT){
			throw new CodeGenException("STACK OVER FLOW CAUSED BY : " + classBinaryName);
		}
		RECURSION_COUNTERS.put(clazz,recursionCounter);

		System.out.println("*********["+recursionCounter+"] Getting impl of " + classBinaryName);
		final String code;
		final String pkg;
		//final String objectName = clazz.getName();
		if(PRIMITIVES.containsKey(clazz)){
			code = PRIMITIVES.get(clazz);
			pkg="impl";
		}else if(clazz.isArray()){
			code = getArrayAsString(clazz);
			Class<?>comp=clazz;
			do{comp=comp.getComponentType();}while(comp.isArray());
			if(CLASSPATH.isRuntimeClass(comp) || comp.getPackage()==null){
				pkg="impl";
			}else{
				pkg=comp.getPackage().getName();
			}
		}else{
			final String packageName = clazz.getPackage().getName();

			final Constructor<?>constructor=new Instantiator().getFirstPublicConstructor(clazz);
			Method factoryCreator=null;
			if(CLASSPATH.isRuntimeClass(clazz)){
				final boolean noDefinedConstrctors=(constructor==null);
				final boolean notInterface = !clazz.isInterface();
				if(notInterface  && noDefinedConstrctors){
					final List<Method>factoryCreators=new Instantiator().getFactoryCreatorMethod(clazz);
					if(factoryCreators.isEmpty()){
						throw new InstantiatorException(clazz+" does not have a public constructor and is a java specific class, and no static member [singleton] is available to create it");
					}else{
						factoryCreator = factoryCreators.get(0);
					}
				}
				pkg = "impl."+packageName;
			}else{
				pkg = packageName;
			}

			//if(constructor==null && factoryCreator==null && clazz.isInterface()){
			if(clazz.isInterface()){
				code = "new " + classBinaryName + "(){\n"+
				getDummyMethodsImplementation(clazz,clazz)+
				"\n\t}";
			}else if(constructor!=null){
				final String params = getParametersDeclaration(constructor.getParameterTypes());
				if(Modifier.isAbstract(constructor.getDeclaringClass().getModifiers())){
					code = "new " + constructor.getName() + "("+params.toString()+"){\n"+
					getDummyMethodsImplementation(clazz,constructor.getDeclaringClass())+
					"\n\t}";
				}else{
					code = "new " + constructor.getName() + "("+params.toString()+")";
				}
			}else if(factoryCreator!=null){
				final String params = getParametersDeclaration(factoryCreator.getParameterTypes());
				code = factoryCreator.getDeclaringClass().getName() +'.'+  factoryCreator.getName() + "("+params+")";
			}else{
				/*
				 * //TODO implement this case (DEAD END)
				 * if the constructor is null, which might be a rare case
				 * then find a factory elsewhere which creates this object
				 */
				//throw new UnsupportedOperationException("this class ["+clazz+"] can never be initialized, sorry");
				code = "("+ classBinaryName +")null";
			}
		}
		RECURSION_COUNTERS.put(clazz,--recursionCounter);
		return new Implementation(pkg,code);
	}
}